"use client"; /** * /share/:id — PUBLIC shared view (no auth; middleware whitelists /share/). * * Data comes from the public endpoint GET /share/:token, which returns the * content inline (page blocks or a database's properties + records) plus a * `canComment` flag. There is no session, so nothing here is editable. * * Why not + directly: both are the *editable* * surfaces — BlockRenderer blocks mount a live Tiptap editor and auto-save via * /blocks/batch (→ 401 for an anonymous visitor), and db-embedded issues * authenticated queries. Exposing an editable surface publicly would be a * security footgun, so this page renders a deliberate **read-only** * presentation of the same data. (Flagged to the core agent: if a shared * read-only BlockRenderer mode is wanted, that's a core change.) */ import { useParams } from "next/navigation"; import { useQuery } from "@tanstack/react-query"; import { apiFetch, type ApiError } from "@/lib/api"; import { valueToText, type DatabaseProperty, type RecordValue } from "@/hooks/use-database"; import { formatRelativeTime } from "@/lib/utils"; // ---- public share payload (apps/api/src/modules/share/share.service.ts) ---- interface ShareBlock { id: string; type: string; content: unknown; properties: Record; position: string; } interface ShareData { share: { id: string; scope: string; allowComments: boolean; expiresAt: string | null }; workspace: { id: string; name: string; slug: string }; canComment: boolean; target: | { type: "page"; page: { id: string; title: string; icon?: string | null; description?: string | null }; blocks: ShareBlock[] } | { type: "database"; database: { id: string; title: string; icon?: string | null; description?: string | null }; properties: { id: string; name: string; type: string; isTitle?: boolean }[]; records: { id: string; properties: Record }[]; }; } /** Recursively pull display text out of a Tiptap doc node. */ function extractText(node: unknown, depth = 0): string { if (depth > 12 || node == null) return ""; if (typeof node === "string") return node; const n = node as { text?: string; content?: unknown[] }; if (typeof n.text === "string") return n.text; if (Array.isArray(n.content)) return n.content.map((c) => extractText(c, depth + 1)).join(""); return ""; } function sortBlocks(blocks: ShareBlock[]): ShareBlock[] { return [...blocks].sort((a, b) => (a.position < b.position ? -1 : a.position > b.position ? 1 : 0)); } function ShareBlockView({ block }: { block: ShareBlock }) { const text = extractText(block.content); const props = block.properties ?? {}; switch (block.type) { case "heading_1": return

{text || "\u00A0"}

; case "heading_2": return

{text || "\u00A0"}

; case "heading_3": return

{text || "\u00A0"}

; case "paragraph": return

{text}

; case "todo": return (
{props.checked ? "✓" : ""} {text}
); case "bulleted_list": return (
{text}
); case "numbered_list": return (
{(props.number as number) ?? "·"} {text}
); case "code": return (
          {text}
        
); case "equation": { const latex = (props.latex as string) ?? text; return latex ?
{latex}
: null; } case "video": case "audio": { const url = (props.url as string) ?? (props.src as string) ?? ""; if (!url) return null; return block.type === "video" ? (